use super::{
    err, err_value, json, load_ready, load_ring, throw_err, Base, Client, ECode, MapSv, RNull,
    TBase, VBase, VMap, Value, VecV,
};
use std::collections::{HashMap, HashSet};
use tracing::{instrument, warn};

type VecS = Vec<String>;
type MSS = HashMap<String, String>;
type BALL = Vec<(String, f64, f64, f64)>; // sn, dw, std, sub

pub type HS = HashSet<String>;

#[derive(Debug)]
pub struct SData {
    pub n_dia: f64,
    pub n_dt: f64,
    pub n_width: f64,
    pub dia: f64,
    pub dt: f64,
    pub width: f64,
    pub tsn: String,
    pub n_box: String,
}

impl SData {
    pub fn new(
        n_dia: f64,
        n_dt: f64,
        n_width: f64,
        dia: f64,
        dt: f64,
        width: f64,
        tsn: String,
        n_box: String,
    ) -> Self {
        Self {
            n_dia,
            n_dt,
            n_width,
            dia,
            dt,
            width,
            tsn,
            n_box,
        }
    }
}

pub struct PairBase {
    pub out_tech: MapSv,
    pub in_tech: MapSv,

    b_ball: f64,
    b_num: i64,
    b_outdia: f64,
    b_outdt: f64,
    b_outwidth: f64,
    b_india: f64,
    b_indt: f64,
    b_inwidth: f64,
    b_dw: f64,
    w_min: f64,
    w_max: f64,
    w: f64,
    diff: f64,
    base: f64,
    range: f64,
    gc: bool,
    pbox: bool,
    usn: String,
    line: String,
    b_mt: String,
    tsn: String,
    tp: String,
    other: Option<String>,
    pbox_outer: HS,
    pbox_inner: HS,
    dr: (bool, f64, f64),
    kr: (bool, f64, f64),
    br: (bool, f64, f64),
}

impl Base for PairBase {}
impl TBase for PairBase {}

impl PairBase {
    pub fn pbox(&self) -> bool {
        self.pbox
    }

    pub fn num(&self) -> i64 {
        self.b_num
    }

    pub fn ball(&self) -> f64 {
        self.b_ball
    }

    pub fn br(&self) -> (bool, f64, f64) {
        self.br
    }

    pub fn dw(&self) -> f64 {
        self.b_dw
    }

    pub fn mt(&self) -> &str {
        &self.b_mt
    }

    pub fn tsn(&self) -> &str {
        &self.tsn
    }

    pub fn para_repair(&mut self, line: &str, data: &MapSv) -> RNull {
        self.line = line.to_string();

        self.tsn = data.ck_str("tsn")?.to_string();
        self.pbox = data.k_bool("pbox").unwrap_or(false);
        self.usn = data.k_str_empty_trim("usn").unwrap_or("").to_string();

        let br = self.get_range(data, "bmin", "bmax");
        self.br = (br.0, br.1 * 1000.0, br.2 * 1000.0);

        Ok(())
    }

    pub fn para(&mut self, line: &str, data: &MapSv) -> RNull {
        self.line = line.to_string();

        self.w_min = 0.0;
        self.w_max = 1.0 / 1000.0;
        self.w = data.ck_f64("w")? / 1000.0;

        self.tsn = data.ck_str("tsn")?.to_string();
        self.other = data.k_str_empty_trim("other").map(|s| s.to_string());

        self.diff = data.ck_f64("diff")? / 1000.0;
        self.base = data.ck_f64("base")? / 1000.0;
        self.range = data.ck_f64("range")? / 1000.0;

        self.dr = self.get_range(data, "dmin", "dmax");
        self.kr = self.get_range(data, "kmin", "kmax");

        self.tp = data.k_str("tp").unwrap_or("both").to_string();
        self.gc = data.k_bool("gc").unwrap_or(false);
        self.pbox = data.k_bool("pbox").unwrap_or(false);
        self.usn = data.k_str_empty_trim("usn").unwrap_or("").to_string();

        Ok(())
    }

    #[instrument(skip_all, name = "save_his")]
    pub async fn save_his(&self, fname: &str, data: &MapSv, client: &Client) -> RNull {
        warn!("step: save calc para");

        let cmd = json!({
            "line": self.line,
            "sn": self.tsn,
            "v": data,
        });

        let _ = client.cfgdb(&cmd, &format!("file/md/{fname}")).await;

        Ok(())
    }

    pub async fn ck_ball_num(&self, sn: &str, client: &Client) -> RNull {
        let tp = self.get_mt_type(&self.b_mt)?;

        let cmd = json!({
            "line": self.line,
            "k": sn,
        });

        let ball_body = client.cfgdb(&cmd, &format!("{tp}/get")).await?;
        let ball_body = ball_body.ck_obj("data")?;

        let num = ball_body.ck_i64("num")?;

        if num < self.b_num {
            throw_err!(ECode::Para, "no ball");
        }

        if num < 1000 * self.b_num {
            let cmd = json!({
                "line": self.line,
                "sn": "alertmsg",
                "v": {
                    format!("miss#{sn}"): num
                }
            });

            let _ = client.plc(&cmd, "put").await;
        }

        Ok(())
    }

    #[instrument(skip_all, name = "get_tech")]
    pub async fn get_tech(&mut self, client: &Client, dw: bool) -> RNull {
        warn!("step: get tech");

        let cmd = json!({
            "line": self.line,
            "k": self.tsn,
        });

        let tech = client.cfgdb(&cmd, "tech/get").await?;
        let tech = tech.ck_obj("data")?;

        self.b_outdia = tech.ck_f64("outdia")?;
        self.b_outdt = tech.ck_f64("outdt")?;
        self.b_outwidth = tech.ck_f64("outwidth")?;

        self.b_india = tech.ck_f64("india")?;
        self.b_indt = tech.ck_f64("indt")?;
        self.b_inwidth = tech.ck_f64("inwidth")?;

        self.b_ball = tech.ck_f64("ball")?;
        self.b_num = tech.ck_i64("num")?;
        self.b_mt = tech.ck_str("mt")?.to_string();

        if dw {
            self.b_dw = tech.ck_f64("dw")?;
        }

        Ok(())
    }

    #[instrument(skip_all, name = "load_try")]
    pub async fn load_try(&self, client: &Client) -> Result<Vec<f64>, Value> {
        let sub = match self.b_mt.as_str() {
            "G" => "stell",
            "T" => "cera",
            _ => throw_err!(ECode::Para, "mt type error"),
        };

        let cmd = json!({
            "line": self.line,
            "sn": sub,
        });

        let cont = client.cfgdb(&cmd, "file/get/tryball").await?;
        let cont = cont.ck_obj("data")?.ck_array("v")?;

        let mut ret = Vec::new();
        for d in cont.iter() {
            let d = d.ckf64()?;
            ret.push(d);
        }

        Ok(ret)
    }

    // (sn, dw)
    #[instrument(skip_all, name = "load_ball")]
    pub async fn load_ball(&self, client: &Client) -> Result<BALL, Value> {
        let sub = match self.b_mt.as_str() {
            "G" => "stell",
            "T" => "cera",
            _ => throw_err!(ECode::Para, "mt type error"),
        };

        let cmd = json!({ "line": self.line });

        let cont = client.cfgdb(&cmd, &format!("{sub}/list")).await?;
        let cont = cont.ck_array("data")?;

        let mut ret = Vec::new();

        for d in cont.iter() {
            let d = d.ckobj()?;

            let state = d.ck_str("state")?;
            let num = d.ck_i64("num")?;

            if state != "ok" || num < self.b_num {
                continue;
            }

            let ball = d.ck_f64("ball")?;
            if !self.is_eq(ball, self.b_ball) {
                continue;
            }

            let std = match d.k_f64("std") {
                Some(v) => v,
                None => {
                    warn!("ball no std: {:?}", d);
                    continue;
                }
            };

            let sub = match d.k_f64("sub") {
                Some(v) => v,
                None => {
                    warn!("ball no sub: {:?}", d);
                    continue;
                }
            };

            let sn = d.ck_str("sn")?;
            let dw = if self.b_mt == "T" { std + sub } else { sub };

            ret.push((sn.to_string(), dw, std, sub));
        }

        Ok(ret)
    }

    #[instrument(skip_all, name = "load")]
    pub async fn load(&mut self, client: &Client, out: &str, inval: &str) -> RNull {
        warn!("step: load outer and inner");

        let (other_inner, other_outer) = match self.other.as_deref() {
            Some(v) => match self.tp.as_str() {
                "inner" => (Some(v), None),
                "outer" => (None, Some(v)),
                _ => (Some(v), Some(v)),
            },
            None => (None, None),
        };

        self.out_tech = load_ring(out, &self.tsn, &self.line, client, other_outer).await?;
        self.in_tech = load_ring(inval, &self.tsn, &self.line, client, other_inner).await?;

        if self.pbox {
            warn!("step: load pbox");

            let _ = self.get_pbox(client).await;
        }

        Ok(())
    }

    pub fn is_outbox(&self, psn: &str) -> bool {
        if !self.pbox {
            return true;
        }

        self.pbox_outer.contains(psn)
    }

    pub fn is_inbox(&self, psn: &str) -> bool {
        if !self.pbox {
            return true;
        }

        self.pbox_inner.contains(psn)
    }

    pub fn box_count(&self, all_log: &mut VecS) {
        if self.pbox {
            all_log.push(format!(
                "out_box: {:?}, in_box: {:?}",
                self.pbox_outer.len(),
                self.pbox_inner.len()
            ));
        }
    }

    // D, De, C
    pub fn out_val(&self, out_data: &Value) -> Option<SData> {
        let out_data = out_data.kobj()?;

        let (n_outdia, n_outdt, n_outwidth, out_tsn, gc, boxout) = self.get_out_val(out_data)?;

        let (outdia, outdt, outwidth) = if gc || self.gc {
            (
                self.round_dia(n_outdia / 1000.0),
                n_outdt / 1000.0,
                n_outwidth / 1000.0,
            )
        } else {
            (
                self.round_dia(n_outdia - self.b_outdia),
                n_outdt - self.b_outdt,
                n_outwidth - self.b_outwidth,
            )
        };

        Some(SData::new(
            n_outdia, n_outdt, n_outwidth, outdia, outdt, outwidth, out_tsn, boxout,
        ))
    }

    pub fn ck_outdia(&self, val: &SData, log: &mut VecS) -> bool {
        if !self.dr.0 {
            return true;
        }

        let b = self.is_range(val.dia, self.dr.1, self.dr.2);

        if !b {
            log.push(format!(
                "外圈直径超限: {:?}, [{:?}, {:?}]",
                val.dia, self.dr.1, self.dr.2
            ));
        }

        b
    }

    pub fn ck_india(&self, val: &SData, log: &mut MSS) -> bool {
        if !self.kr.0 {
            return true;
        }

        let b = self.is_range(val.dia, self.kr.1, self.kr.2);

        if !b {
            log.insert(
                "range".to_string(),
                format!(
                    "内圈直径超限: {:?}, [{:?}, {:?}]",
                    val.dia, self.kr.1, self.kr.2
                ),
            );
        }

        b
    }

    // d, di, B
    pub fn in_val(&self, in_data: &Value) -> Option<SData> {
        let in_data = in_data.kobj()?;

        let (n_india, n_indt, n_inwidth, in_tsn, gc, boxin) = self.get_in_val(in_data)?;

        let (india, indt, inwidth) = if gc || self.gc {
            (
                self.round_dia(n_india / 1000.0),
                n_indt / 1000.0,
                n_inwidth / 1000.0,
            )
        } else {
            (
                self.round_dia(n_india - self.b_india),
                n_indt - self.b_indt,
                n_inwidth - self.b_inwidth,
            )
        };

        Some(SData::new(
            n_india, n_indt, n_inwidth, india, indt, inwidth, in_tsn, boxin,
        ))
    }

    pub async fn set_select_box(&self, data: &VecV, client: &Client) -> RNull {
        for d in data.iter() {
            let d = d.ckobj()?;

            let insn = d.ck_str("insn")?;
            let sn = d.ck_str("sn")?;
            let boxin = d.ck_str("boxin")?;
            let boxout = d.ck_str("boxout")?;

            let cmd = json!({
                "line": self.line,
                "sn": insn,
                "v": {
                    "st": "task",
                }
            });

            client
                .cfgdb(&cmd, &format!("file/md/selbox_{boxin}"))
                .await?;

            let cmd = json!({
                "line": self.line,
                "sn": sn,
                "v": {
                    "st": "task",
                }
            });

            client
                .cfgdb(&cmd, &format!("file/md/selbox_{boxout}"))
                .await?;
        }

        Ok(())
    }

    // sn: (pos, boxsn)
    async fn get_select_box(
        &mut self,
        tp: &str,
        cfg: &MapSv,
        client: &Client,
    ) -> Result<HashMap<String, (String, String)>, Value> {
        let boxv = cfg.ck_array(tp)?;

        let cmd = json!({ "line": self.line });
        let mut ret = HashMap::new();

        for d in boxv.iter() {
            let d = d.ckstr()?;

            let body = client.cfgdb(&cmd, &format!("file/list/selbox_{d}")).await?;
            let body = body.ck_array("data")?;

            for t in body.iter() {
                let t = t.ckobj()?;
                let sn = t.ck_str("sn")?;

                if sn == "attr" {
                    continue;
                }

                let pos = t.ck_str("pos")?;
                if let Some(st) = t.k_str("st") {
                    if st == "task" {
                        continue;
                    }
                }

                ret.insert(sn.to_string(), (pos.to_string(), d.to_string()));
            }
        }

        if ret.is_empty() {
            throw_err!(ECode::EmptyData, "no box sn");
        }

        Ok(ret)
    }

    async fn get_pbox(&mut self, client: &Client) -> RNull {
        let body = load_ready(
            &format!("pbox{}", self.usn),
            Some("box"),
            None,
            &self.line,
            client,
        )
        .await?;

        for (_, v) in body.iter() {
            let v = v.ckobj()?;

            let tp = match self.get_box_tp(v) {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            for (psn, _) in v.iter() {
                if psn == "attr" {
                    continue;
                }

                if tp == "outer" {
                    self.pbox_outer.insert(psn.to_string());
                } else if tp == "inner" {
                    self.pbox_inner.insert(psn.to_string());
                }
            }
        }

        Ok(())
    }

    pub fn ck_width(&self, out: &SData, inval: &SData, log: &mut MSS) -> bool {
        let w_diff = inval.width - out.width;
        let tmp = w_diff.abs();

        if !self.is_le(tmp, self.w) {
            if !self.is_range(w_diff, self.w_min, self.w_max) {
                log.insert(
                    "w".to_string(),
                    format!(
                        "宽度差超限: {:?}, {:?}; {:?}, [{:?}, {:?}]",
                        tmp, self.w, w_diff, self.w_min, self.w_max
                    ),
                );
                return false;
            }
        }

        true
    }

    pub fn ck_dia(&self, out: &SData, inval: &SData, log: &mut MSS) -> bool {
        let tmp = (inval.dia - out.dia).abs();
        if !self.is_le(tmp, self.diff) {
            log.insert(
                "diff".to_string(),
                format!("沟径差超限: {:?}, {:?}", tmp, self.diff),
            );
            return false;
        }

        true
    }

    pub fn ck_ball(&self, ball: &BALL, out: &SData, inval: &SData, log: &mut MSS) -> Option<VecV> {
        let mut ret = Vec::new();

        for (batch_sn, dw, std, sub) in ball.iter() {
            let tmp = (out.dt - inval.dt - 2.0 * dw / 1000.0 - self.base).abs();

            if !self.is_le(tmp, self.range) {
                log.insert("dw".to_string(), format!(
                            "游隙超限: {:?}, range: {:?}, outdt: {:?}, indt: {:?}, dw: {:?}, base: {:?}",
                            tmp, self.range, out.dt, inval.dt, dw / 1000.0, self.base
                        ));
                continue;
            }

            ret.push(json!({
                "sn": batch_sn,
                "ball": self.b_ball,
                "dw": dw,
                "std": std,
                "sub": sub,
                "num": self.b_num,
                "mt": self.b_mt,
            }));
        }

        if ret.is_empty() {
            None
        } else {
            Some(ret)
        }
    }

    pub fn try_ball(&self, ball: &Vec<f64>, out: &SData, inval: &SData) -> Option<VecV> {
        let mut ret = Vec::new();

        for dw in ball.iter() {
            let tmp = (out.dt - inval.dt - 2.0 * dw / 1000.0 - self.base).abs();

            if !self.is_le(tmp, self.range) {
                continue;
            }

            ret.push(json!({
                "ball": self.b_ball,
                "dw": dw,
                "mt": self.b_mt,
            }));
        }

        if ret.is_empty() {
            None
        } else {
            Some(ret)
        }
    }

    pub fn pair_val(
        &self,
        sn: &str,
        insn: &str,
        out: &SData,
        inval: &SData,
        ball: &VecV,
    ) -> Result<MapSv, Value> {
        let mut val = json!({
            "sn": sn,
            "insn": insn,
            "outdia": self.to_m(out.dia),
            "outdt": self.to_m(out.dt),
            "outwidth": self.to_m(out.width),
            "india": self.to_m(inval.dia),
            "indt": self.to_m(inval.dt),
            "inwidth": self.to_m(inval.width),
            "ball": self.b_ball,
            "num": self.b_num,
            "mt": self.b_mt,
            "batch": ball,
            "n_outdia": out.n_dia,
            "n_outdt": out.n_dt,
            "n_outwidth": out.n_width,
            "n_india": inval.n_dia,
            "n_indt": inval.n_dt,
            "n_inwidth": inval.n_width,
            "boxin": inval.n_box,
            "boxout": out.n_box,
        });

        let val = val.ckobj_mut()?;

        if !inval.tsn.is_empty() {
            val.insert("insn_tsn".to_string(), json!(inval.tsn));
        }

        if !out.tsn.is_empty() {
            val.insert("sn_tsn".to_string(), json!(out.tsn));
        }

        Ok(val.clone())
    }

    #[instrument(skip_all, name = "load_select")]
    pub async fn load_select(&mut self, cfg: &MapSv, client: &Client) -> RNull {
        let inv = self.get_select_box("inner", cfg, client).await?;
        let outv = self.get_select_box("outer", cfg, client).await?;

        let tmp = self.load_data("outer", client).await?;

        for (k, v) in tmp.iter() {
            if let Some((pos, boxsn)) = inv.get(k) {
                let mut tmp = v.ckobj()?.clone();

                tmp.insert("pos".to_string(), json!(pos));
                tmp.insert("box".to_string(), json!(boxsn));

                self.out_tech.insert(k.to_string(), json!(tmp));
            }
        }

        let tmp = self.load_data("inner", client).await?;

        for (k, v) in tmp.iter() {
            if let Some((pos, boxsn)) = outv.get(k) {
                let mut tmp = v.ckobj()?.clone();

                tmp.insert("pos".to_string(), json!(pos));
                tmp.insert("box".to_string(), json!(boxsn));

                self.in_tech.insert(k.to_string(), json!(tmp));
            }
        }

        Ok(())
    }

    #[instrument(skip_all, name = "load_new")]
    pub async fn load_new(&mut self, client: &Client) -> RNull {
        warn!("step: load outer and inner");

        self.out_tech = self.load_data("outer", client).await?;
        self.in_tech = self.load_data("inner", client).await?;

        if self.pbox {
            warn!("step: load pbox");

            let _ = self.get_pbox(client).await;
        }

        Ok(())
    }

    async fn load_data(&self, tp: &str, client: &Client) -> Result<MapSv, Value> {
        let mut ret = MapSv::new();

        let cmd = json!({ "line": self.line });

        let body = client.cfgdb(&cmd, &format!("{tp}/list")).await?;
        let body = body.ck_array("data")?;

        for d in body.iter() {
            let d = d.ckobj()?;

            let tsn = match d.k_str("tech") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            if tsn != self.tsn {
                match &self.other {
                    Some(other) => {
                        if tsn != other {
                            continue;
                        }
                    }
                    None => {
                        continue;
                    }
                }
            }

            let psn = match d.k_str("sn") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            let box_sn = match d.k_str_empty_trim("box") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            let (dia, dt, width) = match self.get_val(d) {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            let pre = if tp == "inner" {
                "in"
            } else if tp == "outer" {
                "out"
            } else {
                continue;
            };

            self.to_val(&mut ret, &format!("box{pre}"), psn, json!(box_sn));
            self.to_val(&mut ret, &format!("{pre}dia"), psn, json!(dia));
            self.to_val(&mut ret, &format!("{pre}dt"), psn, json!(dt));
            self.to_val(&mut ret, &format!("{pre}width"), psn, json!(width));
            self.to_val(&mut ret, "gc", psn, json!(true));
        } // for

        Ok(ret)
    }

    fn to_val(&self, cont: &mut MapSv, key: &str, psn: &str, val: Value) {
        match cont.k_obj_mut(psn) {
            Some(v) => {
                v.insert(key.to_string(), val);
            }
            None => {
                cont.insert(psn.to_string(), json!({ key: val }));
            }
        }
    }

    #[instrument(skip_all, name = "save_log")]
    pub async fn save_log(&self, fname: &str, log: VecS, client: &Client) -> RNull {
        warn!("step: save no pair data");

        let cmd = json!({
            "line": self.line,
            "v": log,
        });

        let _ = client
            .cfgdb(&cmd, &format!("field/mdnocache/{}_{}", fname, self.tsn))
            .await;

        Ok(())
    }

    pub fn new() -> Self {
        Self {
            b_ball: 0.0,
            b_num: 0,
            dr: (false, 0.0, 0.0),
            kr: (false, 0.0, 0.0),
            br: (false, 0.0, 0.0),
            b_mt: "".to_string(),
            tsn: "".to_string(),
            tp: "".to_string(),
            out_tech: MapSv::new(),
            in_tech: MapSv::new(),
            b_outdia: 0.0,
            b_outdt: 0.0,
            b_outwidth: 0.0,
            b_india: 0.0,
            b_indt: 0.0,
            b_inwidth: 0.0,
            b_dw: 0.0,
            w_min: 0.0,
            w_max: 0.0,
            w: 0.0,
            diff: 0.0,
            base: 0.0,
            range: 0.0,
            gc: false,
            pbox: false,
            usn: "".to_string(),
            line: "".to_string(),
            other: None,
            pbox_outer: HS::new(),
            pbox_inner: HS::new(),
        }
    }
}
